package net.jescort.lucene.impl;

import net.gelif.kernel.core.exception.SearchException;
import net.jescort.domain.forum.Post;
import net.jescort.domain.forum.Topic;
import net.jescort.lucene.IndexManager;
import net.jescort.lucene.runnable.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.store.SimpleFSLockFactory;
import org.wltea.analyzer.lucene.IKAnalyzer;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Created by IntelliJ IDEA.
 * User: admin@gelif.net
 * Date: 11-8-25
 * Time: 下午3:47
 */
public class IndexManagerImpl implements IndexManager
{
    private IndexReader reader;

    static Log logger = LogFactory.getFactory().getInstance(IndexManagerImpl.class);

    //~ Instance fields
    // ========================================================

    File indexConsistencyMarker;

    private boolean useRAMIndex = false;

    private RAMDirectory fRAMindex;

    private String indexDir = null;

    private boolean inconsistentAtStartup = false;

    private ReadWriteLock rwl = new ReentrantReadWriteLock();

    //~ Constructors
    // ===========================================================

    /**
     * Creates a new search index manager. This should only be created once.
     * Creating the index manager more than once will definately result in
     * errors. The preferred way of getting an index is through the
     * RollerContext.
     *
     */
    protected IndexManagerImpl()
    {
        // we also need to know what our index directory is
        // Note: system property expansion is now handled by WebloggerConfig
        this.indexDir = indexDir.replace('/', File.separatorChar);

        // a little debugging
        logger.info("index dir: " + this.indexDir);

        String test = indexDir + File.separator + ".index-inconsistent";
        indexConsistencyMarker = new File(test);
    }


    /**
     * @inheritDoc
     */
    public void initialize() throws SearchException
    {

        // 1. If inconsistency marker exists.
        //     Delete index
        // 2. if we're using RAM index
        //     load ram index wrapper around index
        //
        if (indexConsistencyMarker.exists())
        {
            getFSDirectory(true);
            inconsistentAtStartup = true;
            logger.debug("Index inconsistent: marker exists");
        } else
        {
            try
            {
                File makeIndexDir = new File(indexDir);
                if (!makeIndexDir.exists())
                {
                    makeIndexDir.mkdirs();
                    inconsistentAtStartup = true;
                    logger.debug("Index inconsistent: new");
                }
                indexConsistencyMarker.createNewFile();
            } catch (IOException e)
            {
                logger.error(e);
            }
        }

        if (indexExists())
        {
            if (useRAMIndex)
            {
                Directory filesystem = getFSDirectory(false);
                try
                {
                    fRAMindex = new RAMDirectory(filesystem);
                } catch (IOException e)
                {
                    logger.error("Error creating in-memory index", e);
                }
            }
        } else
        {
            logger.debug("Creating index");
            inconsistentAtStartup = true;
            if (useRAMIndex)
            {
                fRAMindex = new RAMDirectory();
                createIndex(fRAMindex);
            } else
            {
                createIndex(getFSDirectory(true));
            }
        }

        if (isInconsistentAtStartup())
        {
            logger.info("Index was inconsistent. Rebuilding index in the background...");
            try
            {
                rebuildTopicIndex();
            } catch (RuntimeException e)
            {
                logger.error("ERROR: scheduling re-index operation");
            }
        } else
        {
            logger.info("Index initialized and ready for use.");
        }
    }

    //~ Methods
    // ================================================================

    public void rebuildTopicIndex() throws SearchException
    {
        scheduleIndexOperation(new RebuildTopicIndexRunnable(this, null));
    }

    public void rebuildTopicIndex(Topic topic) throws SearchException
    {
        scheduleIndexOperation(new RebuildTopicIndexRunnable(this, topic));
    }

    public void addPostIndexOperation(Post post) throws SearchException
    {
        AddPostRunnable addPost = new AddPostRunnable(this, post);
        scheduleIndexOperation(addPost);
    }

    public void addPostReIndexOperation(Post post) throws SearchException
    {
        ReIndexPostRunnable reindex = new ReIndexPostRunnable(this, post);
        scheduleIndexOperation(reindex);
    }

    public void removePostIndexOperation(Post post) throws SearchException
    {
        RemovePostRunnable remove = new RemovePostRunnable(this, post);
        executeIndexOperationNow(remove);
    }

    public ReadWriteLock getReadWriteLock()
    {
        return rwl;
    }

    public boolean isInconsistentAtStartup()
    {
        return inconsistentAtStartup;
    }

    /**
     * This is the analyzer that will be used to tokenize comment text.
     *
     * @return Analyzer to be used in manipulating the database.
     */
    public static final Analyzer getAnalyzer()
    {
        return new IKAnalyzer();
    }

    private void scheduleIndexOperation(final IndexRunnable runnable)
    {
        try
        {
            logger.debug("Starting scheduled index operation: " + runnable.getClass().getName());
            roller.getThreadManager().executeInBackground(runnable);
        } catch (InterruptedException e)
        {
            logger.error("Error executing operation", e);
        }
    }

    /**
     * @param runnable
     */
    public void executeIndexOperationNow(final IndexRunnable runnable)
    {
        try
        {
            logger.debug("Executing index operation now: " + runnable.getClass().getName());
            roller.getThreadManager().executeInForeground(runnable);
        } catch (InterruptedException e)
        {
            logger.error("Error executing operation", e);
        }
    }

    public synchronized void resetSharedReader()
    {
        reader = null;
    }

    public synchronized IndexReader getSharedIndexReader()
    {
        if (reader == null)
        {
            try
            {
                reader = IndexReader.open(getIndexDirectory());
            } catch (IOException e)
            {
            }
        }
        return reader;
    }

    /**
     * Get the directory that is used by the search index. This method will
     * return null if there is no index at the directory location. If we are
     * using a RAM index, the directory will be a ram directory.
     *
     * @return Directory The directory containing the index, or null if error.
     */
    public Directory getIndexDirectory()
    {
        if (useRAMIndex)
        {
            return fRAMindex;
        } else
        {
            return getFSDirectory(false);
        }
    }

    private boolean indexExists()
    {
        File fsIndexDir = new File(indexDir);
        Directory directory = null;
        boolean indexExists = false;
        try
        {
            directory = FSDirectory.open(fsIndexDir);
            indexExists = IndexReader.indexExists(directory);
        } catch (IOException e)
        {

        }
        return indexExists;
    }

    Directory getFSDirectory(boolean delete)
    {
        Directory directory = null;

        try
        {
            File fsIndexDir = new File(indexDir);
            directory = FSDirectory.open(fsIndexDir, new SimpleFSLockFactory(fsIndexDir));
            directory = FSDirectory.getDirectory(indexDir, delete);
        } catch (IOException e)
        {
            logger.error("Problem accessing index directory", e);
        }

        return directory;
    }

    private void createIndex(Directory dir)
    {
        IndexWriter writer = null;

        try
        {
            writer = new IndexWriter(dir, IndexManagerImpl.getAnalyzer(), true);
        } catch (IOException e)
        {
            logger.error("Error creating index", e);
        } finally
        {
            try
            {
                if (writer != null)
                {
                    writer.close();
                }
            } catch (IOException e)
            {
            }
        }
    }

    private IndexOperation getSaveIndexOperation()
    {
        return new WriteToIndexOperation(this)
        {
            public void doRun()
            {
                Directory dir = getIndexDirectory();
                Directory fsdir = getFSDirectory(true);

                IndexWriter writer = null;

                try
                {
                    writer = new IndexWriter(fsdir, IndexManagerImpl.getAnalyzer(), true);

                    writer.addIndexes(new Directory[]{dir});
                    indexConsistencyMarker.delete();
                } catch (IOException e)
                {
                    logger.error("Problem saving index to disk", e);

                    // Delete the directory, since there was a problem saving
                    // the RAM contents
                    getFSDirectory(true);
                } finally
                {
                    try
                    {
                        if (writer != null)
                        {
                            writer.close();
                        }
                    } catch (IOException e1)
                    {
                        logger.warn("Unable to close IndexWriter.");
                    }
                }

            }
        };
    }

    public void release()
    {
        // no-op
    }

    public void shutdown()
    {
        if (useRAMIndex)
        {
            scheduleIndexOperation(getSaveIndexOperation());
        } else
        {
            indexConsistencyMarker.delete();
        }

        try
        {
            if (reader != null)
            {
                reader.close();
            }
        } catch (IOException e)
        {
            // won't happen, since it was
        }
    }
}
